/**
 * koa-body - index.js
 * https://github.com/koajs/koa-body
 * http://www.ptbird.cn/koa-body.html
 * https://juejin.cn/post/6919823086170685447
 */
import formidable from './formidable'
import buddy from '../../http/coBody'

const symbolUnparsed = Symbol.for('unparsedBody')
const jsonTypes = [
  'application/json',
  'application/json-patch+json',
  'application/vnd.api+json',
  'application/csp-report',
  'application/reports+json'
]

/**
 *
 * @api public
 * @param opts
 */
export function bodyParser(opts = {}) {
  opts.onError = opts.onError || false
  opts.patchNode = opts.patchNode || false
  opts.patchKoa = opts.patchKoa || true
  opts.multipart = opts.multipart || false
  opts.urlencoded = opts.urlencoded || true
  opts.json = opts.json || true
  opts.text = opts.text || true
  opts.encoding = opts.encoding || 'utf-8'
  opts.jsonLimit = opts.jsonLimit || '1mb'
  opts.jsonStrict = opts.jsonStrict || true
  opts.formLimit = opts.formLimit || '56kb'
  opts.queryString = opts.queryString || null
  opts.formidable = opts.formidable || {}
  opts.includeUnparsed = opts.includeUnparsed || false
  opts.textLimit = opts.textLimit || '56kb'
  opts.parsedMethods = opts.parsedMethods || ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE']
  opts.parsedMethods = opts.parsedMethods.map(method => method.toUpperCase())

  return function(ctx, next) {
    let bodyPromise
    // only parse the body on specifically chosen methods
    if(opts.parsedMethods.includes(ctx.method)) {
      try {
        if(opts.json && ctx.is(jsonTypes)) {
          bodyPromise = buddy.json(ctx, {
            encoding: opts.encoding,
            limit: opts.jsonLimit,
            strict: opts.jsonStrict,
            returnRawBody: opts.includeUnparsed
          })
        } else if(opts.urlencoded && ctx.is('urlencoded')) {
          bodyPromise = buddy.form(ctx, {
            encoding: opts.encoding,
            limit: opts.formLimit,
            queryString: opts.queryString,
            returnRawBody: opts.includeUnparsed
          })
        } else if(opts.text && ctx.is('text/*')) {
          bodyPromise = buddy.text(ctx, {
            encoding: opts.encoding,
            limit: opts.textLimit,
            returnRawBody: opts.includeUnparsed
          })
        } else if(opts.multipart && ctx.is('multipart')) {
          bodyPromise = formy(ctx, opts.formidable)
        }
      } catch(parsingError) {
        if(typeof opts.onError === 'function') {
          opts.onError(parsingError, ctx)
        } else {
          throw parsingError
        }
      }
    }

    bodyPromise = bodyPromise || Promise.resolve({})
    return bodyPromise
      .catch(function(parsingError) {
        if(typeof opts.onError === 'function') {
          opts.onError(parsingError, ctx)
        } else {
          throw parsingError
        }
        return next()
      })
      .then(function(body) {
        if(opts.patchNode) {
          if(isMultiPart(ctx, opts)) {
            ctx.req.body = body.fields
            ctx.req.files = body.files
          } else if(opts.includeUnparsed) {
            ctx.req.body = body.parsed || {}
            if(!ctx.is('text/*')) {
              ctx.req.body[symbolUnparsed] = body.raw
            }
          } else {
            ctx.req.body = body
          }
        }
        if(opts.patchKoa) {
          if(isMultiPart(ctx, opts)) {
            ctx.request.body = body.fields
            ctx.request.files = body.files
          } else if(opts.includeUnparsed) {
            ctx.request.body = body.parsed || {}
            if(!ctx.is('text/*')) {
              ctx.request.body[symbolUnparsed] = body.raw
            }
          } else {
            ctx.request.body = body
          }
        }
        return next()
      })
  }
}

/**
 * Check if multipart handling is enabled and that this is a multipart request
 *
 * @param  {Object} ctx
 * @param  {Object} opts
 * @return {Boolean} true if request is multipart and being treated as so
 * @api private
 */
function isMultiPart(ctx, opts) {
  return opts.multipart && ctx.is('multipart')
}

/**
 * Donable formidable
 *
 * @param  {Stream} ctx
 * @param  {Object} opts
 * @return {Promise}
 * @api private
 */
function formy(ctx, opts) {
  return new Promise(function(resolve, reject) {
    const fields = {}
    const files = {}
    const form =  formidable(opts)
    form.on('end', function() {
      return resolve({
        fields: fields,
        files: files
      })
    }).on('error', function(err) {
      return reject(err)
    }).on('field', function(field, value) {
      if(fields[field]) {
        if(Array.isArray(fields[field])) {
          fields[field].push(value)
        } else {
          fields[field] = [fields[field], value]
        }
      } else {
        fields[field] = value
      }
    }).on('file', function(field, file) {
      if(files[field]) {
        if(Array.isArray(files[field])) {
          files[field].push(file)
        } else {
          files[field] = [files[field], file]
        }
      } else {
        files[field] = file
      }
    })
    if(opts.onFileBegin) {
      form.on('fileBegin', opts.onFileBegin)
    }
    form.parse(ctx.req)
  })
}
